/**
 * Copyright 2010 Tristan Tarrant
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.dataforte.catwalk;

import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.SimpleElementVisitor6;
import javax.tools.Diagnostic;

import net.dataforte.catwalk.annotations.Model;
import net.dataforte.catwalk.annotations.ModelAttribute;

@SupportedAnnotationTypes( { "net.dataforte.catwalk.annotations.Model" })
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class CatwalkModelProcessor extends AbstractProcessor {
	private static final String PROPERTY_PREFIX_GET = "get";
	private static final String PROPERTY_PREFIX_IS = "is";
	private static final String PROPERTY_PREFIX_HAS = "has";

	@Override
	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
		// processingEnv is a predefined member in AbstractProcessor class
		// Messager allows the processor to output messages to the environment
		Messager messager = processingEnv.getMessager();

		// Loop through the annotations that we are going to process
		for (TypeElement typeElement : annotations) {

			// Get the elements that are annotated with Model
			for (Element el : env.getElementsAnnotatedWith(typeElement)) {
				try {
					processAnnotation(el, messager);
				} catch (Exception e) {
					messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
				}
			}
		}

		return true;
	}

	private void processAnnotation(Element element, Messager msg) throws Exception {

		Model model = element.getAnnotation(Model.class);
		String sourceClassName = element.asType().toString();
		int lastDot = sourceClassName.lastIndexOf('.');
		String sourcePackageName = sourceClassName.substring(0, lastDot);
		String sourceShortClassName = sourceClassName.substring(lastDot + 1);

		String targetShortClassName = model.name().replaceAll("#", sourceShortClassName);
		String targetClassName = "".equals(model.classPackage())?sourcePackageName:model.classPackage() + "." + targetShortClassName;

		Writer writer = processingEnv.getFiler().createSourceFile(targetClassName).openWriter();
		PrintWriter pw = new PrintWriter(writer);
		pw.printf("package %s;\n\n", model.classPackage());
		pw.printf("public class %s {\n\n", targetShortClassName);

		// Generate all necessary fields
		List<Attribute> attributes = new ArrayList<Attribute>();

		for (Element enclosedElement : element.getEnclosedElements()) {
			if (enclosedElement.getKind() == ElementKind.METHOD) {
				ModelAttribute modelAttribute = enclosedElement.getAnnotation(ModelAttribute.class);
				if (modelAttribute != null) {
					String methodName = enclosedElement.getSimpleName().toString();
					if(isPropertyName(methodName)) {
						Attribute attribute = new Attribute();
						attribute.sourcePropertyName = getPropertyName(methodName);
						attribute.targetPropertyName = "".equals(modelAttribute.name())?attribute.sourcePropertyName:modelAttribute.name();
						attribute.propertyType = enclosedElement.accept(new MYVisitor(), element);
						attribute.methodName = methodName;
						attribute.initialize = modelAttribute.initialize();
						attributes.add(attribute);						
					}
				}
			}
		}

		// Generate the fields
		for(Attribute attribute : attributes) {
			pw.printf("\tprivate %s %s;\n", attribute.propertyType, attribute.targetPropertyName);
		}
		// Generate two constructors: an empty one and one which takes the
		// source class as parameter
		pw.println();
		pw.printf("\tpublic %s() {}\n\n", targetShortClassName);
		pw.printf("\tpublic %s(%s src) {\n", targetShortClassName, sourceClassName);
		pw.printf("\t\tthis.from%s(src);\n", sourceShortClassName);
		pw.print("\t}\n\n");
		
		// Generate the getters and setters
		for(Attribute attribute : attributes) {
			pw.printf("\t%s %s() {\n", attribute.propertyType, attribute.methodName);
			pw.printf("\t\treturn %s;\n", attribute.targetPropertyName);
			pw.print("\t}\n\n");
			
			pw.printf("\tvoid set%s(%s %s) {\n", capitalize(attribute.targetPropertyName), attribute.propertyType, attribute.targetPropertyName);
			pw.printf("\t\tthis.%s = %s;\n", attribute.targetPropertyName, attribute.targetPropertyName);
			pw.print("\t}\n\n");
		}
		
		// Generate the conversion from the original object to this
		pw.printf("\tpublic %s from%s(%s src) {\n", targetShortClassName, sourceShortClassName, sourceClassName);
		for(Attribute attribute : attributes) {
			pw.printf("\t\tthis.%s = src.%s();\n", attribute.targetPropertyName, attribute.methodName);
			if(attribute.initialize) {
				// attempt to trigger lazy proxy initialization
				pw.printf("\t\tthis.%s.getClass();\n", attribute.targetPropertyName);
			}
		}
		pw.print("\t\treturn this;\n");
		pw.print("\t}\n\n");
		
		// Generate the conversion from this object to the original
		pw.printf("\tpublic %s to%s() {\n", sourceClassName, sourceShortClassName);
		pw.printf("\t\t%s that = new %s();\n", sourceClassName, sourceClassName);
		for(Attribute attribute : attributes) {
			pw.printf("\t\tthat.set%s(this.%s);\n", capitalize(attribute.sourcePropertyName), attribute.targetPropertyName);
		}
		pw.print("\t\treturn that;\n");
		pw.print("\t}\n\n");
		
		writer.write("}");
		writer.flush();
		writer.close();
	}

	public static boolean isPropertyName(String name) {
		return name.startsWith(PROPERTY_PREFIX_GET) || name.startsWith(PROPERTY_PREFIX_IS) || name.startsWith(PROPERTY_PREFIX_HAS);
	}

	public static String getPropertyName(String name) {
		if (!isPropertyName(name)) {
			return null;
		}

		if (name.startsWith(PROPERTY_PREFIX_GET)) {
			name = name.replaceFirst(PROPERTY_PREFIX_GET, "");
		} else if (name.startsWith(PROPERTY_PREFIX_IS)) {
			name = name.replaceFirst(PROPERTY_PREFIX_IS, "");
		} else if (name.startsWith(PROPERTY_PREFIX_HAS)) {
			name = name.replaceFirst(PROPERTY_PREFIX_HAS, "");
		}
		return name.substring(0, 1).toLowerCase() + name.substring(1);
	}
	
	public static String capitalize(String name) {
		return name.substring(0, 1).toUpperCase() + name.substring(1);
	}
	
	private class MYVisitor extends SimpleElementVisitor6<String, Element> {

		@Override
		public String visitExecutable(ExecutableElement e, Element p) {
			return e.getReturnType().toString();
		}		
	}

}
